using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Net.NetworkInformation;

namespace DirectorySerber
{

    struct Client
    {
        public TcpClient Socket;
        public NetworkStream Stream;
        public StreamReader Reader;
        public StreamWriter Writer;
        public string IP;
        public bool CorrectVersion;
    }

    struct PingObject
    {
        public string IP;
        public int I;
        public PingObject(string _IP, int _I)
        {
            IP = _IP;
            I = _I;
        }
    }

    public class Node
    {
        public int ID;
        public int Port;
        public Node(int _ID, int _Port)
        {
            ID = _ID;
            Port = _Port;
        }
    }

    struct Connection
    {
        public TcpClient Socket;
        public int ID;
    }

    class DirectoryServer
    {

        #region Variables

        // Vars
        TcpListener Listener;
        Client[] Clients;
        PortTest PortTester;
        IPList IPs;
        int NoClients;
        bool Loop;
        Ping Ping;

        // Constants
        const int DefaultP2PPort = 31337;
        const string srvVersion = "dev";

        #endregion

        #region Public

        // Constructor
        public DirectoryServer(int _Port)
        {

            // Setup vars etc.
            Listener = new TcpListener(IPAddress.Any, _Port);
            Clients = new Client[256];
            IPs = new IPList();

            // Pinger
            Ping = new Ping();
            Ping.PingCompleted += new PingCompletedEventHandler(CB_Timeout_Response);

            // PortTest'er
            PortTester = new PortTest();
            PortTester.OnSuccess += new PortTest_OnSuccess(CB_PortTest_Success);
            PortTester.OnFail += new PortTest_OnFail(CB_PortTest_Fail);

            // Fill clients
            for (int i = 0; i < Clients.Length; i++)
                Clients[i] = new Client();

            // Start!
            Listener.Start();

        }

        // Start/Loop
        public void Start()
        {

            // Loopz.
            Loop = true;
            do
            {

                // New?
                if (Listener.Pending())
                    AcceptConnection();

                // Data?
                for (int i = 0; i < Clients.Length; i++)
                    if (Clients[i].Socket != null)
                        if (Clients[i].Socket.Connected)
                            if (Clients[i].Stream.DataAvailable)
                                DataPending(i);

                // Cool it.
                System.Threading.Thread.Sleep(10);

            } while (Loop);

        }

        // Stop! :D
        public void Stop()
        {
            Loop = false;
        }

        #endregion

        #region Private
        // Validate IP
        private bool IsValidIP(String IP)
        {

            // Try...
            bool Valid = true;
            try
            {
                IPAddress Temp = IPAddress.Parse(IP);
            }
            catch (Exception)
            {
                Valid = false;
            }
                
            // Valid?
            return Valid;

        }

        // Parse Data
        private void DataPending(int I)
        {

            // Vars
            int i = 0;

            // Read..
            String Data = Clients[I].Reader.ReadLine();

            // Empty?
            if (Data == null)
                return;

            // Parse
            String[] Split = Data.Split(" ".ToCharArray());

            // Switch..
            switch (Split[0])
            {

                // Request IP[s]
                case "REQUEST":



                    // Correct number of params?
                    if (Split.Length != 2)
                        return;

                    // Parse int; careful of incorrect parameters.
                    try
                    {
                        i = int.Parse(Split[1]);
                    }
                    catch (Exception)
                    {

                        // Give an error
                        Clients[I].Writer.WriteLine("ERROR 04 Invalid parameter: REQUEST takes: <int>.");
                        Clients[I].Writer.Flush();

                        // Return; skip this one.
                        return;

                    }

                    CMD_Request(I, i);

                    break;

                // Timeout 
                case "TIMEOUT":

                    // Correct number of params?
                    if (Split.Length != 2)
                        return;

                    // ..
                    CMD_Timeout(I, Split[1]);

                    break;

                // Port test
                case "PORTTEST":


                    // Correct number of params?
                    if (Split.Length != 2)
                        return;

                    // Parse int; careful of incorrect parameters.
                    try
                    {
                        i = int.Parse(Split[1]);
                    }
                    catch (Exception)
                    {

                        // Give an error
                        Clients[I].Writer.WriteLine("ERROR 04 Invalid parameter: PORTTEST takes: <int>.");
                        Clients[I].Writer.Flush();

                        // Return; skip this one.
                        return;

                    }

                    // ..
                    CMD_PortTest(I, i);

                    break;

                // Version..
                case "VERSION":

                    // Correct number of params?
                    if (Split.Length != 2)
                        return;

                    // Call command
                    CMD_Version(I, Split[1]);

                    break;

                // Default
                default:

                    // Send back an error or something.
                    Clients[I].Writer.WriteLine("ERROR 03 Unknown command.");
                    Clients[I].Writer.Flush();

                    break;

            }

        }

        // Find free Socket
        private int FindFreeSocket()
        {

            // :)
            int I = 0;
            for (int i = 0; i < 255; i++)
                if (Clients[i].Socket == null)
                {
                    I = i;
                    break;
                }
                else
                    if (Clients[i].Socket.Connected == false)
                    {
                        I = i;
                        break;
                    }

            // Return
            return I;

        }

        // Accept socket
        private void AcceptConnection()
        {

            // Too many clients?
            if (NoClients == Clients.Length)
                return; // We'll keep trying till there is a space.

            // Accept

            // Find free
            int I = FindFreeSocket();

            // Get socket client
            Clients[I].Socket = Listener.AcceptTcpClient();
            Clients[I].Stream = Clients[I].Socket.GetStream();
            Clients[I].Reader = new StreamReader(Clients[I].Stream);
            Clients[I].Writer = new StreamWriter(Clients[I].Stream);
            Clients[I].IP = ((IPEndPoint)Clients[I].Socket.Client.RemoteEndPoint).Address.ToString();
            AddIPToPool(I); // Add to list

            // Client already connected? -- Need to check after already connected
            // Don't know how to get IP from Listener otherwise.
            for (int i = 0; i < Clients.Length; i++)
                if ((i != I) && (Clients[i].IP == Clients[I].IP) && (Clients[i].Socket != null))
                    if (Clients[i].Socket.Connected == true)
                    {

                        // Tell old connection
                        Clients[i].Writer.WriteLine("ERROR Client has opened new connection; closing this old one.");
                        Clients[i].Writer.Flush();

                        // Drop old connection
                        Clients[i].Socket.Close();
                        Clients[i].IP = String.Empty;
                        Clients[i].Reader = null;
                        Clients[i].Writer = null;

                        break;

                    }

            // Increase connection count
            NoClients++;

        }

        // Adds an IP to our pool of IPs
        private void AddIPToPool(int I)
        {

            // Register :)
            IPs.Add(Clients[I].IP, DefaultP2PPort);

        }

        #endregion

        #region Command Callbacks

        // When a Port test was done successfully.
        private void CB_PortTest_Success(TcpClient C)
        {

            // Send "PORTTESTSUCCESS" down new port
            StreamWriter W = new StreamWriter(C.GetStream());
            W.WriteLine("PORTTESTSUCCESS"); // Fugly.
            W.Flush();

            // Close!
            C.Close();
            W.Close();
            W = null;

        }

        // When a Port test failed.
        private void CB_PortTest_Fail(int ID)
        {

            // Send an error down original stream
            Clients[ID].Writer.WriteLine("ERROR 02 Port is blocked.");
            Clients[ID].Writer.Flush();

        }

        // Ping response
        private void CB_Timeout_Response(object sender, PingCompletedEventArgs e)
        {

            // Error?
            if (e.Error != null)
            {

                // Get Ping Object
                PingObject O = (PingObject)e.UserState;

                // Is the ip in the list? Are they trying to TIMEOUT their own IP? 
                // Baad.
                if ((IPs.Contains(O.IP)) && (O.IP != Clients[O.I].IP))
                    IPs.Delete(O.IP);

            }

        }

        #endregion

        #region Commands

        // [Pre-handshake] Checks client's version & server's is compatible.
        private void CMD_Version(int I, string Ver)
        {

            // Check against our version
            if (Ver.ToLower() == srvVersion.ToLower())
                Clients[I].CorrectVersion = true;
            else
            {

                // Incorrect version!

                // Error
                Clients[I].Writer.WriteLine("ERROR 05 Version mismatch. Server is using version: " + srvVersion.ToString());
                Clients[I].Writer.Flush();
            }

        }

        // [Post-handshake] Checks client's P2P port is open
        private void CMD_PortTest(int I, int Port)
        {

            // Start test
            PortTester.AsnycTest(Clients[I].IP, Port, I);

        }

        // [Post-Handshake] -- Sent to server when a client finds an IP to be dead.
        private void CMD_Timeout(int I, string IP)
        {

            // Start a ping!
            Ping.SendAsync(IP, (object)(new PingObject(IP, I)));

        }

        // [Post-Handshake] -- Requests a list of IPs from Server
        private void CMD_Request(int I, int Count)
        {

            // No IPs in pool?
            if (IPs.Count == 1) // That'll be client! so it's not == 0
            {

                // Return an error
                Clients[I].Writer.WriteLine("ERROR 01 No IPs in Pool.");
                Clients[I].Writer.Flush();

                return;

            }

            // Get some IPs
                // Check we have enough IPs to fulfil request
                // Let's limit to a nice value too
                // In future, need to limit to like 5 IPs per day..
                // Anonymity reasons etc.
                if (Count >= 5)
                        Count = 5;
                if (Count > IPs.Count)
                    Count = IPs.Count;
                // Vars
                string[] _IPs = new string[Count];
                int Got = 0, Rand;
                Random R = new Random((int)System.DateTime.Now.Ticks);
                // Loop
                while (Got < Count)
                {

                    // Get a random number
                    Rand = R.Next(0, IPs.Count);

                    // Is there an IP here?
                    if (IPs.Contains(Rand))
                    {

                        // Already got it?
                        bool GotIt = false;
                        for (int i = 0; i < Got; i++)
                            if (_IPs[i] == IPs.GetIP(Rand))
                                GotIt = true;

                        // Not already got it?
                        if (!GotIt)
                        {

                            // Get it!
                            _IPs[Got] = IPs.GetIP(Rand);
                            Got++;

                        }

                    }
                    
                }

            // We should now have an array of IPs to return in _IPs..
            // let's send back in the form "IPS <xxx> <xxx> <xxx>\r\n"

            // Array to string
            String sIPs = String.Empty;
            foreach (String IP in _IPs)
                sIPs += IP + " ";
            sIPs = sIPs.Trim();

            // Send
            Clients[I].Writer.WriteLine("IPS " + sIPs);
            Clients[I].Writer.Flush();

        }

        #endregion

    }
}
